```tex
\newcommand{\commonfolder}{../../common-files}
\input{\commonfolder/header}
\input{\commonfolder/copyright}

\newcommand{\telnet} {\texttt{telnet}\xspace}
\newcommand{\iptables}{\texttt{iptables}\xspace}
\newcommand{\netfilter}{\texttt{netfilter}\xspace}
\newcommand{\Netfilter}{\texttt{Netfilter}\xspace}

\newcommand{\firewallFigs}{./Figs}
\lhead{\bfseries SEED Labs -- Firewall Exploration Lab}

%\newcommand{\pointleft}[1]{\reflectbox{\ding{217}} \textbf{\texttt{#1}}}

\begin{document}

\begin{center}
{\LARGE 防火墙探索实验}
\end{center}

\seedlabcopyright{2006 - 2021}

% *******************************************
% SECTION
% ******************************************* 
\section{概述}

本实验的学习目标有两个：了解防火墙的工作原理，以及如何为网络设置一个简单的防火墙。学生首先将实现一个简单的状态无感知的包过滤型防火墙，该防火墙会检查每个传入和传出的包，并根据管理员设定的防火墙策略决定是丢弃还是转发这些包。通过此实现任务，学生可以了解防火墙的基本工作原理。

实际上，Linux 已经自带了一个基于 \texttt{netfilter} 的内置防火墙，称为 \iptables。在本实验中，我们将使用简单的网络拓扑，并要求学生利用 \iptables 设置防火墙规则以保护该网络。学生们还将接触到 \iptables 的其他一些有趣的用法。本实验涵盖以下主题：

\begin{itemize}[noitemsep]
    \item 防火墙
    \item Netfilter
    \item 可加载内核模块 (LKM)
    \item 使用 \iptables 设置防火墙规则
    \item \iptables 的各种应用
\end{itemize}

\paragraph{阅读材料和视频。}
关于防火墙的详细内容可以在以下章节中找到：

\begin{itemize}
    \item SEED 书籍第17章，\seedbook
    \item SEED 讲义第9节，\seedisvideo
\end{itemize}

\paragraph{实验环境。} \seedenvironmentC

% *******************************************
% SECTION
% ******************************************* 
\section{使用容器设置实验环境}


在本实验中，我们需要使用多个机器。其设置如图~\ref{fig:labsetup} 所示。我们将使用容器来设置此实验环境。

\begin{figure}[htb]
    \centering
    \includegraphics[width=0.8\textwidth]{./Figs/TwoLANs.pdf}
    \caption{实验环境设置}
    \label{fig:labsetup}
\end{figure}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{容器的设置和命令}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\input{\commonfolder/container/setup}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
% 本实验似乎不需要此部分内容，所以先将其注释掉。
%\subsection{从 \texttt{192.168.60.0/24} 网络中分离虚拟机} 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%\input{\commonfolder/container/detach_from_interface}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% *******************************************
% SECTION
% ******************************************* 
\section{任务 1：实现一个简单的防火墙}

在本任务中，我们将实现一种简单的包过滤型防火墙，它会检查每个传入和传出的包，并强制执行管理员设定的防火墙策略。由于包处理是在内核中进行的，因此包过滤也需要在内核中完成。因此，似乎要实现这样的防火墙需要修改 Linux 内核。在过去，这必须通过修改并重新构建内核来完成。现代 Linux 操作系统提供了几种新的机制，以无需重建内核镜像就可操作包。这两种机制是 \textit{Loadable Kernel Module} (LKM) 和 \texttt{Netfilter}。

\paragraph{关于容器的注意事项。}
由于所有容器共享同一个内核，因此内核模块是全局的。因此，如果我们在某个容器中设置了内核模块，则会对其它容器和主机产生影响。出于这个原因，在本实验中我们将从主机虚拟机设置内核模块。

另一个需要注意的是，容器的 IP 地址都是虚拟的。传送到这些虚拟 IP 地址的包可能并不会按照 Netfilter 文档中的路径进行传输。因此，在本任务中为了避免混淆，我们尽量避免使用那些虚拟地址。我们在主机虚拟机上执行大多数任务，而容器则主要用于其它任务。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 1.A：实现一个简单的内核模块}

\texttt{LKM} 允许在运行时向内核添加新的模块。这个新模块能够扩展内核的功能，无需重新构建内核或重启计算机。防火墙的包过滤部分可以作为 LKM 实现。在本任务中我们将熟悉 LKM。

以下是一个简单的加载式内核模块（该模块文件包含在实验设置文件中）。当模块被加载时会打印出 \texttt{"Hello World!"}，当模块从内核移除时会打印出 \texttt{"Bye-bye World!"}。这些消息不会显示在屏幕上；实际上会被打印到 \texttt{/var/log/syslog} 文件中。您可以使用 \texttt{"dmesg"} 命令来查看这些消息。

\begin{lstlisting}[language=C, caption=\texttt{hello.c}（包含在实验设置文件中）]
#include <linux/module.h>
#include <linux/kernel.h>

int initialization(void)
{
    printk(KERN_INFO "Hello World!\n");
    return 0;
}

void cleanup(void)
{
    printk(KERN_INFO "Bye-bye World!.\n");
}

module_init(initialization);
module_exit(cleanup);
\end{lstlisting}

现在需要创建一个 \texttt{Makefile}，其中包含以下内容（该文件包含在实验设置文件中）。只需输入 \texttt{make} 命令，上面的程序就会被编译成可加载内核模块。如果要将下面的内容复制粘贴到 \texttt{Makefile} 中，请确保替换 \texttt{make} 命令前后的空格为一个制表符。

\begin{lstlisting}
obj-m += hello.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
\end{lstlisting}

生成的内核模块位于 \texttt{hello.ko}。可以使用以下命令来加载模块、列出所有模块和移除模块。您还可以通过 \texttt{"modinfo hello.ko"} 命令查看 Linux 内核模块的信息。

\begin{lstlisting}
$ sudo insmod hello.ko     (插入一个模块)
$ lsmod | grep hello       (列出模块)
$ sudo rmmod hello         (移除模块)
$ dmesg                    (检查消息)
$ sudo dmesg    (在 Ubuntu 22.04 中，此命令需要超级用户权限)
\end{lstlisting}

\paragraph{任务。}请将这个简单的内核模块编译并运行在您的虚拟机上。本任务中我们将不使用容器。请在实验报告中展示您的运行结果。

\paragraph{注意事项。}如果您在一个基于 Ubuntu 22.04 的 Apple Silicon 设备上编译内核模块，可能会看到一个错误信息，指出 \texttt{gcc-12} 未安装。可以使用以下命令进行安装：
\begin{lstlisting}
$ sudo apt install gcc-12
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 1.B：使用 \texttt{Netfilter} 实现一个简单的防火墙}


在本任务中，我们将编写我们的包过滤程序作为一个 LKM，并将它插入到内核中的包处理路径。在过去，在引入 \netfilter 到 Linux 内核之前，这是一件不容易的事情。

\texttt{Netfilter} 设计目的是为了授权用户方便地操作包。它通过在 Linux 内核中实现一系列钩子来达到这个目标。这些钩子被插入到各种位置，包括传入和传出包的路径。如果我们想操纵传入的包，我们只需要将自己程序（内嵌于 LKM 中）连接到相应的钩子即可。一旦一个传入的包到达某个钩子时，我们的程序就会被调用。我们的程序可以决定这个包是否需要阻止；此外，我们还可以在程序中修改这些包。

在这个任务中，您需要用 LKM 和 \texttt{Netfilter} 实现一个包过滤模块。此模块将从数据结构获取防火墙策略，并使用策略来决定是否应阻拦某些包。希望学生能够专注于过滤部分，即防火墙的核心所在，因此允许学生在程序中硬编码防火墙策略。关于如何使用 \texttt{Netfilter} 的详细指南可以在 SEED 书籍的第17章找到。我们也会提供一些指导。

\paragraph{\texttt{Netfilter} 钩子绑定。}
使用 \netfilter 很容易。我们需要做的就是将我们的函数（在内核模块中）绑定到相应的 \netfilter 钩子。这里展示了一个示例（代码位于 \path{Labsetup/packet_filter} 中，但可能与下面的示例有所不同）。

代码结构遵循之前实现的内核模块结构。当将该内核模块添加到内核时，代码中的 \texttt{registerFilter()} 函数将会被调用。在这个函数中，我们将注册两个钩子至 \netfilter。

要注册一个钩子，您需要准备一个钩子数据结构，并设置所需的所有参数，其中最重要的是一个函数名（Line~\ding{202}）和一个钩子编号（Line~\ding{203}）。该钩子编号是 \netfilter 中的 5 个钩子之一，在此示例中，当一个包到达 \texttt{LOCAL\_IN} 钩子时，将会调用函数 \texttt{printInfo()}（这个函数稍后给出）。一旦准备好了钩子数据结构，我们将在 Line~\ding{204} 中将其绑定到 \netfilter。

\begin{lstlisting}[language=C, caption={注册挂钩函数到 \netfilter}]
static struct nf_hook_ops hook1, hook2;

int registerFilter(void) {
   printk(KERN_INFO "Registering filters.\n");
   
   // Hook 1
   hook1.hook = printInfo;                    (*@\ding{202}@*)
   hook1.hooknum = NF_INET_LOCAL_IN;          (*@\ding{203}@*)
   hook1.pf = PF_INET;
   hook1.priority = NF_IP_PRI_FIRST;
   nf_register_net_hook(&init_net, &hook1);   (*@\ding{204}@*)

   // Hook 2
   hook2.hook = blockUDP;
   hook2.hooknum = NF_INET_POST_ROUTING;
   hook2.pf = PF_INET;
   hook2.priority = NF_IP_PRI_FIRST;
   nf_register_net_hook(&init_net, &hook2);

   return 0;
}

void removeFilter(void) {
   printk(KERN_INFO "The filters are being removed.\n");
   nf_unregister_net_hook(&init_net, &hook1);
   nf_unregister_net_hook(&init_net, &hook2);
}

module_init(registerFilter);
module_exit(removeFilter);
\end{lstlisting}

\paragraph{\texttt{Ubuntu 20.04} 虚拟机注意事项：}
第二版 SEED 书籍中的代码是在 Ubuntu 16.04 下开发的。需要对其进行稍作修改才能在 Ubuntu 20.04 上运行。这些更改出现在挂钩注册和取消注册 API 中。请参阅以下差异：

\begin{lstlisting}[language=C]
// 钩子注册：
  nf_register_hook(&nfho);                  // 在 Ubuntu 16.04 虚拟机上
  nf_register_net_hook(&init_net, &nfho);   // 在 Ubuntu 20.04 虚拟机上

// 钩子取消注册：
  nf_unregister_hook(&nfho);                // 在 Ubuntu 16.04 虚拟机上
  nf_unregister_net_hook(&init_net, &nfho); // 在 Ubuntu 20.04 虚拟机上
\end{lstlisting}

\paragraph{挂钩函数。}我们给出了一个示例挂钩函数，它仅打印包信息。当 \netfilter 调用挂钩函数时，会向该函数传递三个参数，包括指向实际包的指针 (\texttt{skb})。
在下面的代码中，Line~\ding{202} 显示了如何从 \texttt{state} 参数中提取挂钩编号。在线~\ding{203} 中，我们使用 \texttt{ip\_hdr()} 函数获取 IP 头部的指针，并在线~\ding{204} 使用 \texttt{\%pI4} 格式字符串指定符来打印源和目的IP地址。

\begin{lstlisting}[language=C, caption={挂钩函数示例}]
unsigned int printInfo(void *priv, struct sk_buff *skb,   
                       const struct nf_hook_state *state)
{
   struct iphdr *iph;
   char *hook;

   switch (state->hook){          (*@\ding{202}@*)
     case NF_INET_LOCAL_IN: 
          printk("*** LOCAL_IN"); break;
     .. (代码省略) ...
   }

   iph = ip_hdr(skb);             (*@\ding{203}@*)           
   printk("    %pI4  --> %pI4\n", &(iph->saddr), &(iph->daddr)); (*@\ding{204}@*)
   return NF_ACCEPT;
}
\end{lstlisting}

如果需要获取其他协议的头部，请可以使用定义在各种头文件中的以下函数。这些头部结构的定义可以在 \path{/lib/modules/5.4.0-54-generic/build/include/uapi/linux} 文件夹中找到，其中路径中的版本号是通过运行 \texttt{"uname -r"} 获取的结果，因此如果内核版本不同，则可能会有所不同。

\begin{lstlisting}[language=C]
struct iphdr   *iph   = ip_hdr(skb)    // 需要包含 <linux/ip.h> 
struct tcphdr  *tcph  = tcp_hdr(skb)   // 需要包含 <linux/tcp.h>
struct udphdr  *udph  = udp_hdr(skb)   // 需要包含 <linux/udp.h>
struct icmphdr *icmph = icmp_hdr(skb)  // 需要包含 <linux/icmp.h>
\end{lstlisting}

\paragraph{阻止包。}我们还提供了一个示例挂钩函数，以展示如何根据指定的条件阻止一个包。以下示例阻止目的 IP 地址为 \texttt{8.8.8.8}、目的端口为 \texttt{53} 的 UDP 包（即，阻止 DNS 查询到名称服务器 8.8.8.8）。

\begin{lstlisting}[language=C, caption={代码示例：阻止UDP}]
unsigned int blockUDP(void *priv, struct sk_buff *skb,
                 const struct nf_hook_state *state)
{
   struct iphdr *iph;
   struct udphdr *udph;
   u32  ip_addr;
   char ip[16] = "8.8.8.8";

   // 将 IPv4 地址从点分十进制格式（即，字符串，如 1.2.3.4）转换为一个 32 位二进制数
   in4_pton(ip, -1, (u8 *)&ip_addr, '\0', NULL);                  (*@\ding{202}@*)

   iph = ip_hdr(skb);
   if (iph->protocol == IPPROTO_UDP) {
       udph = udp_hdr(skb);                                         
       if (iph->daddr == ip_addr && ntohs(udph->dest) == 53){     (*@\ding{203}@*)
            printk(KERN_DEBUG "****Dropping %pI4 (UDP), port %d\n", 
                              &(iph->daddr), port);                 
            return NF_DROP;                                       (*@\ding{204}@*)
        }
   }
   return NF_ACCEPT;                                              (*@\ding{205}@*)
}
\end{lstlisting}

在上述代码中，Line~\ding{202} 展示了，在内核中如何将 IPv4 地址（即字符串格式，如 1.2.3.4）转换为一个 32 位二进制数 (0x01020304)，以与包内部存储的二进制数进行比较。Line~\ding{203} 比较目的 IP 地址和端口号是否符合我们指定规则中的值，如果它们匹配，则返回 \texttt{NF\_DROP} 通知 \netfilter 要丢弃该包；否则返回 \texttt{NF\_ACCEPT}（这仅表示该包被此挂钩函数接受，但可能仍会被其它挂钩函数拦截）。

\paragraph{任务。}完整的示例代码被称为 \texttt{seedFilter.c}，包含在实验设置文件中 (位于 \path{Files/packet_filter} 文件夹内)。请完成以下任务（每个单独进行）：

1. 使用提供的 \texttt{Makefile} 编译样本代码，并将其加载到内核中，演示防火墙是否按预期工作。可以使用以下命令生成发送到 8.8.8.8 的 UDP 包 (这是 Google 的 DNS 服务器)。如果您的防火墙正常运行，则您的请求将被阻止；否则您会收到响应。

\begin{lstlisting}
dig @8.8.8.8 www.example.com 
\end{lstlisting}

2. 将 \texttt{printInfo} 函数绑定到所有 \netfilter 钩子。以下是钩子编号的宏定义：

\begin{lstlisting}
NF_INET_PRE_ROUTING 
NF_INET_LOCAL_IN        
NF_INET_FORWARD 
NF_INET_LOCAL_OUT 
NF_INET_POST_ROUTING    
\end{lstlisting}

3. 实现两个新的挂钩以实现以下功能：
   - 阻止其他计算机向虚拟机 ping 信息。
   - 阻止其他计算机通过 telnet 连接到该虚拟机。请为每个挂钩函数编写不同的代码，但将它们注册到同一个 \netfilter 钩子中。您需要决定使用哪个钩子。telnet 的默认端口是 TCP 端口 23。您可以启动容器，在 10.9.0.5 上运行以下命令（IP 地址 10.9.0.1 是虚拟机分配的 IP 地址；为了简化起见，可以将其硬编码在防火墙规则中）：

\begin{lstlisting}
ping 10.9.0.1
telnet 10.9.0.1
\end{lstlisting}

\paragraph{重要注意事项：}由于我们修改了内核，有很大的机会会导致内核崩溃。确保您经常备份文件以免丢失。系统崩溃的一个常见原因是忘记注销挂钩。当模块被移除时，这些挂钩仍然会被触发，但模块不再存在于内核中，这将导致系统崩溃。为了避免这种情况，请确保对每个添加到模块中的挂钩，在 \texttt{removeFilter} 中添加一行以注销它，这样当模块被移除时，那些挂钩也会被删除。

% *******************************************
% SECTION
% ******************************************* 
\section{任务 2：无状态防火墙规则的实验}

在前一个任务中，我们有机会使用 \netfilter 构建了一个简单的防火墙。实际上，Linux 已经自带了一个基于 \netfilter 的内置防火墙，称为 \iptables。技术上讲，防火墙的内核部分实现被称为 \texttt{Xtables}，而 \iptables 是用于配置该防火墙的用户空间程序。然而，在实际使用中，\iptables 通常指的是内核部分和用户空间程序的整体。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{\iptables 的背景}

在本任务中，我们将使用 \iptables 来设置防火墙。 iptables 防火墙不仅用于过滤包，还能够对包进行修改。为了管理这些防火墙规则以满足不同的需求，iptables 将所有规则组织成一个分层结构：表、链和规则。
有多个表格，每个表格都规定了主要的规则目的（见表~\ref{firewall:table:iptables}）。例如，用于过滤包的规则应该放在 \texttt{filter} 表中，而用于对包进行修改的规则应放在 \texttt{nat} 或 \texttt{mangle} 表中。

每个表格包含多个链，每个链对应一个 \netfilter 钩子。基本上，每条链指示其规则在何处被强制执行。例如，在 \texttt{FORWARD} 链上的规则将在 \texttt{NF\_INET\_FORWARD} 钩子处被强制执行；而在 \texttt{INPUT} 链上的规则则会在 \texttt{NF\_INET\_LOCAL\_IN} 钩子处被强制执行。

每条链包含一组将被执行的防火墙规则。当我们设置防火墙时，我们将规则添加到这些链中。例如，如果我们想阻止所有传入的 telnet 通信，则需要向 \texttt{filter} 表中的 \texttt{INPUT} 链添加一条规则。如果我们希望将所有传入的 telnet 重定向到另一主机上不同的端口（即进行端口转发），则可以向 \texttt{mangle} 表中 \texttt{INPUT} 链添加一条规则，因为我们需要对包进行修改。

\begin{table}[htb]
        \centering
%       \renewcommand{\arraystretch}{1.2}
        \caption{\iptables 表和链}
        \label{firewall:table:iptables}
        \centering

        \begin{tabular}{|l|l|l|}
                \hline
                \bfseries 表 & \bfseries 链 & \bfseries 功能 \\
                \hline\hline
                filter          &    \texttt{INPUT}      & 包过滤 \\
                                &    \texttt{FORWARD}    & \\
                                &    \texttt{OUTPUT}      & \\
                \hline
                nat             &   \texttt{PREROUTING}    & 修改源或目的网络地址 \\
                                &   \texttt{INPUT}      & \\
                                &   \texttt{OUTPUT}      & \\
                                &   \texttt{POSTROUTING}   & \\
                \hline
                mangle          &   \texttt{PREROUTING}    & 修改包内容 \\
                                &   \texttt{INPUT}      & \\
                                &   \texttt{FORWARD}     & \\
                                &   \texttt{OUTPUT}      & \\
                                &   \texttt{POSTROUTING}   & \\
                \hline
        \end{tabular}
\end{table}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{\iptables 的使用}


要向每个表格中的链添加规则，我们可以使用 \iptables 命令，这是一个非常强大的命令。学生可以通过键入 \texttt{"man iptables"} 查看手册或从网上找到许多教程。
使 \iptables 复杂的是，在使用该命令时需要提供的大量命令行选项。然而，如果我们理解这些命令行参数的结构，我们会发现该命令并不复杂。

在典型的 \iptables 命令中，我们向某个表格中的某条链添加规则或将规则从某条链删除，所以我们需要指定一个表名（默认为 \texttt{filter}）、一条链名和对这条链进行的操作。之后，我们需要指定规则，即基本上是将与每个通过的包匹配的一个模式。
该命令的一般结构如下面所示：

\begin{lstlisting}
iptables -t <table> -<operation> <chain>  <rule>   -j <target>
         ---------- --------------------  -------  -----------
            表          链           规则      动作
\end{lstlisting}

规则是 \iptables 命令中最复杂的部分。当我们在下面使用特定的规则时，会提供更多详细信息。在以下内容中列出了一些常用的命令：

\begin{lstlisting}
// 列出某表格中的所有规则（不显示行号）
iptables -t nat -L -n

// 列出某表格中的所有规则（带有行号）
iptables -t filter -L -n --line-numbers


// 从过滤表的 INPUT 链中删除第2条规则 
iptables -t filter -D INPUT 2

// 拒绝所有满足<规则>的传入包
iptables -t filter -A INPUT <rule>  -j DROP
\end{lstlisting}

\paragraph{注意事项。} Docker 依赖 \iptables 来管理它创建的网络，因此会向 \texttt{nat} 表中添加许多规则。
当我们操作 \iptables 规则时，应小心不要删除 Docker 的规则。例如，运行 \texttt{"iptables -t nat -F"} 命令将会非常危险，因为它将移除 \texttt{nat} 表中的所有规则，包括许多 Docker 的规则。这会带来麻烦给 Docker 容器。对于 \texttt{filter} 表来说，则没有问题，因为 Docker 并不会触碰这个表。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务 2.A：保护路由器}

在本任务中我们将设置规则以阻止外部机器访问路由器（除了 ping）。请在路由器容器上执行以下 \iptables 命令，然后尝试从 \texttt{10.9.0.5} 访问该路由器。 (1) 能否 ping 通路由器？(2) 是否能通过 telnet 连接到路由器（所有容器都运行着一个 telnet 服务器；在它们上创建了一个名为 \texttt{seed} 的账户，密码是 \texttt{dees}）。请报告您的观察结果并解释每条规则的目的。

\begin{lstlisting}
iptables -A INPUT  -p icmp --icmp-type echo-request -j ACCEPT
iptables -A OUTPUT -p icmp --icmp-type echo-reply   -j ACCEPT
iptables -P OUTPUT DROP     (*@\pointleft{设置 OUTPUT 的默认规则}@*)
iptables -P INPUT  DROP     (*@\pointleft{设置 INPUT 的默认规则}@*)
\end{lstlisting}

\paragraph{清理。} 在继续下一个任务前，请通过以下命令将 \texttt{filter} 表恢复到初始状态：

\begin{lstlisting}
iptables -F
iptables -P OUTPUT ACCEPT
iptables -P INPUT  ACCEPT
\end{.lstlisting}

另一种方法是重新启动容器，您可以使用以下命令（您需要先找到该容器的 ID）：

\begin{lstlisting}
$ docker restart <Container ID>
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务 2.B：保护内部网络} 

在本任务中我们需要在路由器上设置防火墙规则以保护内部网络 \texttt{192.168.60.0/24}。我们需使用 FORWARD 链来实现此目的。

INPUT 和 OUTPUT 链的方向是明确的：传入（对于 INPUT）或传出（对于 OUTPUT）。但对于 FORWARD 链，情况并非如此：因为它具有双向性——前往内部网络或传出外部网络的所有包都经过这个链。为了指定方向，可以使用 \texttt{"-i xyz"}（从 \texttt{xyz} 接口进入）和/or \texttt{"-o xyz"}（通过 \texttt{xyz} 接口离开）。内部和外部网络的接口不同。
您可以使用 \texttt{"ip addr"} 命令来查找这些接口名称。

在本任务中，我们希望实现一个防火墙以保护该内部网络。更具体地说，我们需要对 ICMP 通信施加以下限制：

\begin{enumerate}[noitemsep]
    \item 外部主机不能 ping 内部主机。
    \item 外部主机可以 ping 路由器。
    \item 内部主机可以 ping 外部主机。
    \item 所有其他内网与外网之间的包都应被阻拦。
\end{enumerate}

您需要使用 \texttt{"-p icmp"} 选项来指定与 ICMP 协议相关的匹配选项。您可以运行 \texttt{"iptables -p icmp -h"} 查看所有 ICMP 匹配选项。以下示例将丢弃 ICMP 消息请求。

\begin{lstlisting}
iptables -A FORWARD -p icmp --icmp-type echo-request -j DROP
\end{lstlisting}

在您的实验报告中，请包含您设置的规则和演示防火墙是否按预期工作的一些截图。
完成此任务后，请记得清理表格或重启容器，再进行下一个任务。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务 2.C：保护内部服务器} 

在此任务中我们要保护内网 (\texttt{192.168.60.0/24}) 中的 TCP 服务。具体来说，我们需要实现以下目标：

\begin{enumerate}[noitemsep]
    \item 内部所有主机都运行一个 telnet 服务器（监听端口 23）。外部主机只能访问该网段中 192.168.60.5 上的 telnet 服务，而不能访问其他内部主机上的服务。
    
    \item 外部主机不能访问任何其他内部服务器。

    \item 内部主机可以访问所有内部服务器。

    \item 内部主机不能访问外部服务器。

    \item 在此任务中不允许使用连接跟踪机制。该功能将在后续任务中使用。
\end{enumerate}

您需要使用 \texttt{"-p tcp"} 选项来指定与 TCP 协议相关的匹配选项。您可以运行 \texttt{"iptables -p tcp -h"} 查看所有 TCP 匹配选项。以下示例允许来自接口 eth0 的源端口为 5000 的 TCP 数据包通过。

\begin{lstlisting}
iptables -A FORWARD -i eth0 -p tcp --sport 5000  -j ACCEPT
\end{lstlisting}

完成后，请记得清理表格或重启容器，再进行下一个任务。



% *******************************************
% SECTION
% ******************************************* 
\section{任务 3：连接跟踪与状态防火墙}

在上一个任务中，我们仅设置了无状态的防火墙，检查每个包独立地独立。然而，数据包通常是相互关联的；它们可能是 TCP 连接的一部分或由其他数据包触发的 ICMP 包。将它们独立处理不考虑这些包的上下文，会导致不准确、不安全或复杂的防火墙规则。例如，如果希望允许只有建立连接后 TCP 数据包才能进入我们的网络，则使用无状态包过滤器是难以实现的，因为当防火墙检查每个单独的 TCP 数据包时，它无法知道该数据包是否属于现有连接，除非它为每个连接维护一些状态信息。
如果做到这一点，这将变成一个状态防火墙。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 3.A：实验连接跟踪}

为了支持状态防火墙，我们需要能够跟踪连接。这个功能是由内核中的 \texttt{conntrack} 机制实现的。在这个任务中我们将进行一系列与该模块相关的实验，并熟悉连接跟踪机制。
在我们的实验中，我们将在路由器容器上检查连接跟踪信息。这可以通过以下命令完成：

\begin{lstlisting}
# conntrack -L
\end{lstlisting}

本任务的目标是通过一系列实验帮助学生理解此跟踪机制中的连接概念，尤其是对于 ICMP 和 UDP 协议来说，因为它们不像 TCP 那样具有连接。
请进行以下实验。对每个实验，请描述您的观察结果及解释。

1. **ICMP 实验：** 运行如下命令并在路由器上检查连接跟踪信息。描述您的观察情况。ICMP 连接状态保持多久？

\begin{lstlisting}
// 在 10.9.0.5 上发送 ICMP 包
# ping 192.168.60.5
\end{lstlisting}

2. **UDP 实验：** 运行如下命令并在路由器上检查连接跟踪信息。描述您的观察情况。UDP 连接状态保持多久？

\begin{lstlisting}
// 在 192.168.60.5 上启动 netcat UDP 服务器
# nc -lu 9090

// 在 10.9.0.5 向 192.168.60.5:9090 发送 UDP 包  
# nc -u 192.168.60.5 9090
<输入一些内容，然后按回车>
\end{lstlisting}

3. **TCP 实验：** 运行如下命令并在路由器上检查连接跟踪信息。描述您的观察情况。TCP 连接状态保持多久？

\begin{lstlisting}
// 在 192.168.60.5 启动 netcat TCP 服务器
# nc -l 9090

// 在 10.9.0.5 发送 TCP 包 
# nc 192.168.60.5 9090
<输入一些内容，然后按回车>
\end{lstlisting}

\paragraph{任务。} 完整的示例代码被称为 \texttt{seedFilter.c}，包含在实验设置文件中 (位于 \path{Files/packet_filter} 文件夹内)。请完成以下任务（每个单独进行）：

1. 使用提供的 \texttt{Makefile} 编译样本代码，并将其加载到内核中以验证防火墙是否按预期工作。您可以使用以下命令生成发送到 8.8.8.8 的 UDP 包 (这是 Google 的 DNS 服务器)。如果您的防火墙正常运行，则您的请求将被阻止；否则您会收到响应。

\begin{lstlisting}
dig @8.8.8.8 www.example.com 
\end{lstlisting}

2. 将 \texttt{printInfo} 函数绑定到所有 \netfilter 钩子。以下是钩子编号的宏定义：

\begin{lstlisting}
NF_INET_PRE_ROUTING 
NF_INET_LOCAL_IN        
NF_INET_FORWARD 
NF_INET_LOCAL_OUT 
NF_INET_POST_ROUTING    
\end{lstlisting}

3. 实现两个新的挂钩以实现以下功能：
   - 阻止其他计算机 ping 路由器。
   - 阻止其他计算机通过 telnet 连接到路由器。请为每个挂钩函数编写不同的代码，但将它们注册到同一个 \netfilter 钩子中。您需要决定使用哪个钩子。telnet 的默认端口是 TCP 端口 23。您可以启动容器，在 10.9.0.5 上运行以下命令（IP 地址 10.9.0.1 是虚拟机分配的 IP 地址；为了简化起见，可以将其硬编码在防火墙规则中）：

\begin{lstlisting}
ping 10.9.0.1
telnet 10.9.0.1
\end{lstlisting}

请记住，在完成此任务后要清除所有规则。

\paragraph{重要注意事项：}由于我们修改了内核，有很大几率会导致内核崩溃。确保您经常备份文件以免丢失。系统崩溃的一个常见原因是忘记注销挂钩。当模块被移除时，这些挂钩仍然会被触发，但模块不再存在于内核中，这将导致系统崩溃。为了避免这种情况，请确保对每个添加到模块中的挂钩，在 \texttt{removeFilter} 中添加一行以注销它，这样当模块被移除时，那些挂钩也会被删除。

% *******************************************
% SECTION
% ******************************************* 
\section{任务 4：限制网络流量}

除了阻止包外，我们还可以限制通过防火墙的包数量。这可以通过 \iptables 的 \texttt{limit} 模块实现。在本任务中我们将使用该模块来限制从 \texttt{10.9.0.5} 到内部网的包的数量。您可以键入以下命令查看手册：

\begin{lstlisting}
$ iptables -m limit -h
limit 匹配选项：
--limit avg             最大平均匹配速率：默认每小时 3 次 [除非后面带有 /sec /minute /hour /day 后缀，表示每秒、分钟等]
                        被匹配的包数量
--limit-burst number    匹配次数上限，默认值为 5
\end{lstlisting}

请在路由器上运行以下命令，并从 \texttt{10.9.0.5} ping \texttt{192.168.60.5}。描述您的观察结果。请分别进行实验并解释是否需要第二条规则以及原因。

\begin{lstlisting}
iptables -A FORWARD -s 10.9.0.5 -m limit \
         --limit 10/minute --limit-burst 5 -j ACCEPT

iptables -A FORWARD -s 10.9.0.5 -j DROP
\end{lstlisting}

% *******************************************
% SECTION
% *******************************************
\section{任务 5：负载均衡}

iptables 非常强大，除了防火墙外，它还有许多其他应用。我们不可能在本实验中涵盖所有这些应用，但我们将尝试其中一个应用：负载均衡。在此任务中，我们将使用它来实现内部网络上运行的三个 UDP 服务器之间的负载均衡。首先，在每个主机 \texttt{192.168.60.5}、\texttt{192.168.60.6} 和 \texttt{192.168.60.7} 上启动这些服务器（通过 -k 选项表示该服务器可以接受来自多个主机的 UDP 数据报）：

\begin{lstlisting}
nc -luk 8080
\end{lstlisting}

我们可以通过使用 \texttt{statistic} 模块来实现负载均衡。您可以键入以下命令获取其手册。您可以看到有两个模式：\texttt{random} 和 \texttt{nth}。我们将分别使用这两种模式进行实验。

\begin{lstlisting}
$ iptables -m statistic -h 
statistic 匹配选项：
 --mode mode         匹配模式（随机、第 n 个）
 random 模式：
[!] --probability p  概率
 nth 模式：
[!] --every n        匹配每 n 个包
 --packet p          初始计数器值 (0 <= p <= n-1, 默认为 0)
\end{lstlisting}

\paragraph{使用 \texttt{nth} 模式（轮询）。}
在路由器容器中，设置以下规则，适用于所有发送到端口 8080 的 UDP 包。这里我们使用了 \texttt{statistic} 模块的 \texttt{nth} 模式：每三个包中选择第一个（即第一个包），将其目标 IP 地址和端口号改为 \texttt{192.168.60.5:8080}。修改后的包将继续其旅程。

\begin{lstlisting}
iptables -t nat -A PREROUTING -p udp --dport 8080      \
         -m statistic --mode nth --every 3 --packet 0  \
         -j DNAT --to-destination 192.168.60.5:8080
\end{lstlisting}

需要注意的是，未匹配到该规则的包将继续其旅程；它们不会被修改或阻止。有了这条规则后，如果您向路由器的 8080 端口发送 UDP 包，则可以看到其中每三个包中有一个会到达 \texttt{192.168.60.5}。

\begin{lstlisting}
// 在 10.9.0.5
echo hello | nc -u 10.9.0.11 8080
<hit Ctrl-C>
\end{lstlisting}

请在路由器容器中添加更多规则，以便所有三个内部主机都能接收到等量的包。请提供这些规则的一些解释。

\paragraph{使用 \texttt{random} 模式。}
让我们使用不同的模式来实现负载均衡。以下规则会选择匹配一个具有概率 P 的数据包。您需要将 P 替换为一个概率数值。

\begin{lstlisting}
iptables -t nat -A PREROUTING -p udp --dport 8080   \
         -m statistic --mode random --probability P \
         -j DNAT --to-destination 192.168.60.5:8080
\end{lstlisting}

请使用此模式来实现您的负载均衡规则，使每个内部服务器都能获得大致相同的流量（虽然不一定完全相同，但在总包数足够大时应非常接近）。请提供一些关于这些规则的解释。

% *******************************************
% SECTION
% ******************************************* 
\section{提交和演示}


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\input{\commonfolder/submission}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\end{document}
```